﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.IO;
using UnnamedPodcastLibrary;
using System.Collections;
using System.Xml;
using System.ComponentModel;
using System.Net;
using System.Diagnostics;

namespace UnnamedPodcastLibrary
{
    public class Manager
    {
        public Manager(string dataDirectory, string stateFileName)
        {
            this._episodeDownloadQueue = new Queue<EpisodeDownloadJob>();
            this._episodeDownloadQueueLock = new object();

            this._feedDownloadQueue = new Queue<DownloadJob>();
            this._feedDownloadQueueLock = new object();

            this._imageDownloadQueue = new Queue<ImageDownloadJob>();
            this._imageDownloadQueueLock = new object();

            this._postProcessQueue = new Queue<Job>();
            this._postProcessQueueLock = new object();

            this._syncQueue = new Queue<EpisodeDownloadJob>();
            this._syncQueueLock = new object();

            this._episodeDownloader = new System.Net.WebClient();
            this._episodeDownloader.DownloadProgressChanged += new System.Net.DownloadProgressChangedEventHandler(DownloaderProgressChanged);
            this._episodeDownloader.DownloadFileCompleted += new System.ComponentModel.AsyncCompletedEventHandler(DownloaderFileCompleted);

            this._feedDownloader = new System.Net.WebClient();
            this._feedDownloader.DownloadProgressChanged += new System.Net.DownloadProgressChangedEventHandler(DownloaderProgressChanged);
            this._feedDownloader.DownloadFileCompleted += new System.ComponentModel.AsyncCompletedEventHandler(DownloaderFileCompleted);

            this._imageDownloader = new WebClient();
            this._imageDownloader.DownloadProgressChanged += DownloaderProgressChanged;
            this._imageDownloader.DownloadFileCompleted += DownloaderFileCompleted;

            this.DataDirectory = dataDirectory;
            this.StateFileName = stateFileName;
        }

        void DownloaderProgressChanged(object sender, DownloadProgressChangedEventArgs e)
        {
            var job = (DownloadJob)e.UserState;
            job.StepsComplete = e.BytesReceived;
            job.StepsPercentComplete = e.ProgressPercentage;
            job.StepsTotal = e.TotalBytesToReceive;

            this.OnChanged(new ManagerChangedEventArgs{Kind = ManagerEventKind.FileDownloadingProgress,Job = job});
        }

        void DownloaderFileCompleted(object sender, AsyncCompletedEventArgs e)
        {
            var job = (DownloadJob)e.UserState;

            if (e.Cancelled
                || e.Error != null)
            {
                job.Error = e.Error;

                this.OnChanged(new ManagerChangedEventArgs { Kind = ManagerEventKind.FileDownloadCanceled, Job = job });

                //clean up the partial file
                if(File.Exists(job.LocalFileName))
                    File.Delete(job.LocalFileName);
            }
            else
            {
                //update state info for this download
                lock (this._podcasts.Lock)
                {
                    var imageJob = job as ImageDownloadJob;
                    if (imageJob != null)
                    {
                       //TODO: process images
                    }
                    else
                    {
                        var feedJob = job as FeedDownloadJob;
                        if (feedJob != null)
                        {
                            feedJob.Feed = FeedReader.ReadFeed(feedJob.DownloadUri, feedJob.LocalFileName);
                            this._podcasts.SetFeedState(feedJob.Feed.DownloadUrl, FeedState.DownloadCompleted);
                        }
                        else
                        {
                            var episodeJob = job as EpisodeDownloadJob;
                            if (episodeJob != null)
                                this._podcasts.SetEpisodeState(episodeJob.Episode.UniqueId, EpisodeState.DownloadCompleted);
                        }
                    }
                }

                //execute and post download action
                if (job.DownloadedAction != null)
                {
                    job.DownloadedAction(job);
                }

                //notify anyone who cares
                this.OnChanged(new ManagerChangedEventArgs { Kind = ManagerEventKind.FileDownloaded, Job = job});

                //send this job off to processing
                this.AddToProcessQueue(job);
            }
        }


        #region Configuration
        public string StateFileName { get; private set; }
        public string SyncDirectory { get; set; }
        public string DataDirectory { get; private set; }
        public Int32 FeedCheckMinutes { get; set; }
        public Int32 DownloadThreadIdleMilliseconds { get; set; }
        public Int32 ProcessThreadIdleMilliseconds { get; set; }
        public string FeedDownloadDirectory { get { return Path.Combine(this.DataDirectory, "Feeds"); } }
        public bool SkipVideoEpisodes { get; set; }
        #endregion

        #region Fields
        PodcastData _podcasts;
        DateTime _lastFeedRefresh;

        readonly object _episodeDownloadQueueLock;
        readonly Queue<EpisodeDownloadJob> _episodeDownloadQueue;

        readonly object _feedDownloadQueueLock;
        readonly Queue<DownloadJob> _feedDownloadQueue;

        readonly object _imageDownloadQueueLock;
        readonly Queue<ImageDownloadJob> _imageDownloadQueue;

        Thread _downloadThread;
        Thread _postProcessThread;

        readonly object _postProcessQueueLock;
        Queue<Job> _postProcessQueue;

        readonly object _syncQueueLock;
        Queue<EpisodeDownloadJob> _syncQueue;

        readonly WebClient _episodeDownloader;
        readonly WebClient _feedDownloader;
        readonly WebClient _imageDownloader;

        bool _shutdownThreads;
        #endregion

        #region Events
        public event EventHandler<ManagerChangedEventArgs> Changed;
        void OnChanged(ManagerChangedEventArgs args)
        {
            if (this.Changed != null)
                this.Changed(this, args);
        }

        void OnChanged(ManagerEventKind kind, Job job)
        {
            this.OnChanged(new ManagerChangedEventArgs { Kind = kind, Job = job });
        }

        public event EventHandler<FeedInfoChangedEventArgs> FeedInfoChanged;
        void OnFeedInfoChanged(FeedInfoChangedEventArgs args)
        {
            if (this.FeedInfoChanged != null)
                this.FeedInfoChanged(this, args);
        }
        #endregion

        public void Start()
        {
            string statePathFileName = null;
            try
            {
                if (!Directory.Exists(this.DataDirectory))
                    Directory.CreateDirectory(this.DataDirectory);

                if (!Directory.Exists(this.FeedDownloadDirectory))
                    Directory.CreateDirectory(this.FeedDownloadDirectory);

                statePathFileName = Path.Combine(this.DataDirectory, this.StateFileName);
                this._podcasts = new PodcastData(statePathFileName);
                lock (this._podcasts.Lock)
                {
                    this._podcasts.Load();
                }
            }
            catch (System.IO.IOException ex)
            {
                this.OnChanged(ManagerEventKind.ManagerStopped, new Job { Error = ex, Message = "Exception caught while starting: " + ex.Message });
                return;
            }

            this._postProcessThread = new Thread(new ThreadStart(this.ConsumeProcessQueue)) {Name = "Process Queue Thread" };
            this._downloadThread = new Thread(new ThreadStart(this.ConsumeDownloadQueue)) { Name = "Download Queue Thread"};

            this._postProcessThread.Start();
            this._downloadThread.Start();

            this.OnChanged(new ManagerChangedEventArgs { Kind = ManagerEventKind.ManagerStarted, Job = new Job { Message = "Manager started." } });

            this.ResumeEpisodesToSync();
            this.ResumeEpisodesToDownload();
        }

        void ResumeEpisodesToSync()
        {
            lock (this._podcasts.Lock)
            {
                var toSync = from episode in this._podcasts.GetEpisodesDownloaded()
                             where File.Exists(GetEpisodeLocalFileName(episode))
                             select episode;

                foreach (var episode in toSync)
                {
                    this.AddToSyncQueue(new EpisodeDownloadJob 
                    {
                        Episode = episode, 
                        LocalFileName = GetEpisodeLocalFileName(episode),
                    });
                }
            }
        }

        void ResumeEpisodesToDownload()
        {
            lock (this._podcasts.Lock)
            {
                foreach (var episode in this._podcasts.GetEpisodesToDownload())
                {
                    this.AddEpisodeToDownloadQueue(episode);
                }
            }
        }

        void PushAllFeeds()
        {
            lock (this._podcasts.Lock)
            {
                foreach (var feed in this._podcasts.Feeds)
                {
                    foreach (var episode in feed.Episodes)
                    {
                        this.OnFeedInfoChanged(new FeedInfoChangedEventArgs { Feed = feed, Episode = episode });
                    }
                }
            }

        }

        public void AddFeed(Uri feed)
        {
            this.OnChanged(ManagerEventKind.AddFeedStarted, new Job {Message = string.Format("Adding feed '{0}'", feed.OriginalString) });
            lock (this._podcasts.Lock)
            {
                try
                {
                    this._podcasts.AddFeed(feed);
                    this.OnFeedInfoChanged(new FeedInfoChangedEventArgs {Feed = this._podcasts.FindFeed(feed) });
                }
                catch (IOException ex)
                {
                    this.OnChanged(ManagerEventKind.AddFeedFinished, new Job { Error = ex, Message = "Exception caught while attempting to add a feed to monitor." });
                    return;
                }

                this.AddToFeedDownloadQueue(new FeedDownloadJob
                {
                    DownloadUri = feed,
                    LocalFileName = Path.Combine(this.FeedDownloadDirectory, GetSafeString(feed.AbsoluteUri)) + ".xml",
                    DownloadedAction = NotifyProgress,
                    ProcessingAction = this.DownloadNewEpisodes
                });
            }

            this.OnChanged(ManagerEventKind.AddFeedFinished, new Job { Message = string.Format("Feed '{0}' added successfully.", feed.OriginalString) });
        }

        public void ImportFeeds(string opmlPathFileName)
        {
            this.AddToProcessQueue(new LocalFileJob { LocalFileName = opmlPathFileName, ProcessingAction = ImportFeeds });
        }

        public void ExportFeeds(string opmlPathFileName)
        {
            this.AddToProcessQueue(new LocalFileJob { LocalFileName = opmlPathFileName, ProcessingAction = ExportFeeds });
        }

        void ImportFeeds(Job localFileJob)
        {
            var job = (LocalFileJob)localFileJob;
            try
            {
                this.OnChanged(ManagerEventKind.ImportFeedsStarted, job);

                var feeds = FeedReader.ReadOpml(job.LocalFileName);
                if (feeds.Count() == 0)
                {
                    job.Message = "No feeds found.";
                    this.OnChanged(ManagerEventKind.ImportFeedsFinished, job);
                    return;
                }

                var feedsTotal = feeds.Count();
                var currentFeed = 0;
                foreach (var feedUri in feeds)
                {
                    this.AddFeed(feedUri);

                    job.StepsTotal = feedsTotal;
                    job.StepsComplete = ++currentFeed;
                    job.StepsPercentComplete = CalculatePercentComplete(currentFeed, feedsTotal);
                    this.OnChanged(ManagerEventKind.ImportFeedsProgress, job);
                }

                job.Message = string.Format("{0} feeds imported.", feedsTotal);
                this.OnChanged(ManagerEventKind.ImportFeedsFinished, job);
            }
            catch (IOException ex)
            {
                job.Error = ex;
                job.Message = "Exception caught while attempting to import feeds:" + ex.Message;
                this.OnChanged(ManagerEventKind.ImportFeedsFinished, job);
            }
        }

        void ExportFeeds(Job localFileJob)
        {
            var job = (LocalFileJob)localFileJob;
            try
            {
                this.OnChanged(ManagerEventKind.ExportFeedsStarted, job);
                
                lock (this._podcasts.Lock)
                {
                    this._podcasts.ExportSubscriptions(job.LocalFileName);
                }
                
                this.OnChanged(ManagerEventKind.ExportFeedsFinished, job);
            }
            catch (IOException ex)
            {
                job.Error = ex;
                job.Message = "Could not export subscriptions: " + ex.Message;
                this.OnChanged(ManagerEventKind.ExportFeedsFinished, job);
            }
        }


        //public void DownloadEpisode(Episode episode)
        //{
        //    lock (this._podcasts.Lock)
        //    {
        //        this._podcasts.RemoveEpisode(episode);
        //        this.AddEpisodeToDownloadQueue(episode);
        //    }
        //    this.OnChanged(ManagerEventKind., Message="File download request acknowledged.");
        //}

        string GetEpisodeLocalFileName(Episode episode)
        {
            return
                Path.Combine(Path.Combine(this.DataDirectory, GetSafeString(episode.FeedTitle)), GetSafeString(episode.Title)) + ".mp3";
        }

        string GetFeedLocalFileName(Feed feed)
        {
            return 
                Path.Combine(this.FeedDownloadDirectory, string.Format("{0}_{1}_{2}", 
                Guid.NewGuid(),
                GetSafeString(feed.DownloadUrl.AbsoluteUri), 
                ".xml"));
        }


        void AddEpisodeToDownloadQueue(Episode episode)
        {
            lock (this._episodeDownloadQueueLock)
            {
                    this.AddToEpisodeDownloadQueue(new EpisodeDownloadJob
                    {
                        DownloadingAction = CreateFeedDirectory,
                        DownloadUri = episode.DownloadUrl,
                        LocalFileName = GetEpisodeLocalFileName(episode),
                        Episode = episode,
                        ProcessingAction = this.AddToSyncQueue
                    });
            }
        }

        public void UpdateFeeds()
        {
            lock (this._podcasts.Lock)
            {
                try
                {
                    var totalFeeds = this._podcasts.Feeds.Count();

                    this.AddToFeedDownloadQueue(new DownloadJob 
                    { 
                        Message = "Update feeds started.", 
                        StepsNotifyKind = ManagerEventKind.UpdateFeedsStarted,
                        ProcessingAction = this.NotifyProgress,
                        StepsTotal = totalFeeds});

                    var currentStep = 0;
                    foreach (var feed in this._podcasts.Feeds)
                    {
                        var percentComplete = CalculatePercentComplete(++currentStep, totalFeeds);

                        this.AddToFeedDownloadQueue(new FeedDownloadJob
                        {
                            StepsNotifyKind = ManagerEventKind.UpdateFeedsProgress,
                            DownloadUri = feed.DownloadUrl,
                            DownloadedAction = this.NotifyProgress,
                            LocalFileName = GetFeedLocalFileName(feed),
                            ProcessingAction = this.DownloadNewEpisodes,
                            //progress info:
                            Message = string.Format("Update feeds {0}% complete.", percentComplete),
                            StepsComplete = percentComplete,
                            StepsPercentComplete = percentComplete,
                            StepsTotal = totalFeeds,
                        });
                    }

                    this.AddToFeedDownloadQueue(new FeedDownloadJob { ProcessingAction = NotifyProgress, Message = "Update feeds complete.", StepsComplete = this._podcasts.Feeds.Count(), StepsTotal = this._podcasts.Feeds.Count()});
                }
                catch (IOException ex)
                {
                    this.OnChanged(ManagerEventKind.Error, new Job { Error = ex, Message = ex.Message });
                }
            }
        }

        void NotifyProgress(Job downloadJob)
        {
            var job = (DownloadJob)downloadJob;
            this.OnChanged(job.StepsNotifyKind, job);
        }

        static Int32 CalculatePercentComplete(Int32 currentStep, Int32 totalSteps)
        {
            if (currentStep == 0
                || totalSteps == 0)
                return 0;
            return Convert.ToInt32((Convert.ToDecimal(currentStep) / Convert.ToDecimal(totalSteps)) * 100);
        }

        void DownloadNewEpisodes(Job feedDownloadJob)
        {
            var job = (FeedDownloadJob)feedDownloadJob;
            lock (this._podcasts.Lock)
            {
                try
                {
                    if(job.Feed.Episodes.Any())
                    {
                        this._podcasts.UpdateFeed(job.Feed);

                        //see about getting an image for the feed
                        if (job.Feed.ImageUrl != null)
                        {
                            job.Feed.ImageLocalFileName = Path.Combine(this.FeedDownloadDirectory, GetSafeString(job.Feed.ImageUrl.OriginalString)) + ".image";
                            if (!string.IsNullOrEmpty(job.Feed.DownloadUrl.OriginalString))
                            {
                                this.AddToImageDownloadQueue(new ImageDownloadJob
                                {
                                    DownloadUri = job.Feed.ImageUrl,
                                    LocalFileName = job.Feed.ImageLocalFileName,
                                    Feed = job.Feed,
                                    ProcessingAction = this.UpdateFeedImage
                                });
                            }
                        }
                    }
                }
                catch (FileNotFoundException ex)
                {
                    job.Error = ex;
                    job.Message = string.Format("File not found while processing feed update: {0}", ex.Message);
                    this.OnChanged(ManagerEventKind.UpdateFeedFinished, job);
                    return;
                }
                catch (XmlException ex)
                {
                    job.Error = ex;
                    job.Message = "Feed xml is not correctly formed: " + ex.Message;
                    this.OnChanged(ManagerEventKind.UpdateFeedFinished, job);
                    return;
                }

                if (job.Feed.Episodes.Count() == 0)
                {
                    job.Message = "No new episodes.";
                    this.OnChanged(ManagerEventKind.UpdateFeedFinished,job);
                    return;
                }

                var newEpisodes = from episode in job.Feed.Episodes
                                  where episode.State == EpisodeState.InfoDownloaded
                                  select episode;

                foreach (var newEpisode in newEpisodes)
                {
                    this.AddEpisodeToDownloadQueue(newEpisode);
                    this.OnFeedInfoChanged(new FeedInfoChangedEventArgs {Episode = newEpisode });
                }
                job.Message = string.Format("{0} new episodes found.", newEpisodes.Count());
                this.OnChanged(ManagerEventKind.UpdateFeedFinished, job);
            }
        }

        void UpdateFeedImage(Job imageDownloadJob)
        {
            var job = (ImageDownloadJob)imageDownloadJob;

            if (job.Error != null)
            {
                this.OnChanged(ManagerEventKind.Error, job);
                return;
            }
            if (!File.Exists(job.LocalFileName))
            {
                job.Message = "Image file is expected to exist locally but does not.";
                this.OnChanged(ManagerEventKind.Error, job);
                return;
            }

            this.OnFeedInfoChanged(new FeedInfoChangedEventArgs {Feed = job.Feed });
        }
            
        static string GetSafeString(string s)
        {
            string safe = s;
            foreach (var c in ((IList)Path.GetInvalidFileNameChars()).Cast<char>().Concat(Path.GetInvalidPathChars()).Concat(new[] { '.',':' }))
            {
                safe = safe.Replace(new string(new[] { c }), string.Empty);
            }
            return safe;
        }

        void CreateFeedDirectory(Job localFileJob)
        {
            var job = (LocalFileJob)localFileJob;
            try
            {
                var directory = Path.GetDirectoryName(job.LocalFileName);
                if (!Directory.Exists(directory))
                    Directory.CreateDirectory(directory);
            }
            catch (IOException ex)
            {
                job.Error = ex;
                this.OnChanged(ManagerEventKind.Error, job);
            }
        }

        void AddToSyncQueue(Job episodeDownloadJob)
        {
            var job = (EpisodeDownloadJob)episodeDownloadJob;
            lock (this._syncQueueLock)
            {
                this._syncQueue.Enqueue(job);
                
                job.Message = "File available to sync.";
                this.OnChanged(ManagerEventKind.SyncFileAvailable, job);
            }
        }

        public void Sync()
        {
            this.AddToProcessQueue(new ProgressJob {ProcessingAction = this.Sync });
        }

        void Sync(Job progressJob)
        {
            var syncJob = (ProgressJob)progressJob;
            lock (this._syncQueueLock)
            {
                var filesToSync = this._syncQueue.Count;
                syncJob.Message = "Sync episodes started.";
                syncJob.StepsPercentComplete = 0;
                syncJob.StepsComplete = 0;
                syncJob.StepsTotal = filesToSync;
                this.OnChanged(ManagerEventKind.SyncStarted, syncJob);

                if (!Directory.Exists(this.SyncDirectory))
                {
                    syncJob.Message = string.Format("Sync target directory '{0}' does not exist.", this.SyncDirectory);
                    syncJob.StepsTotal = filesToSync;
                    syncJob.StepsComplete = 0;
                    syncJob.StepsPercentComplete = 100;
                    this.OnChanged(ManagerEventKind.SyncFinished, syncJob);
                    return;
                }

                while (this._syncQueue.Count > 0)
                {
                    Int32 stepsComplete = 0;

                    var syncEpisodeJob = this._syncQueue.Dequeue();

                    syncJob.Message = "Syncing episode.";
                    syncJob.StepsComplete = stepsComplete;
                    syncJob.StepsTotal = filesToSync;
                    syncJob.StepsPercentComplete = CalculatePercentComplete(stepsComplete, filesToSync);
                    this.OnChanged(ManagerEventKind.SyncProgress, syncJob);

                    syncEpisodeJob.Message = "Sync episode started.";
                    syncEpisodeJob.StepsComplete = 0;
                    syncEpisodeJob.StepsPercentComplete = 0;
                    syncEpisodeJob.StepsTotal = 1;
                    this.OnChanged(ManagerEventKind.SyncFileStarted, syncEpisodeJob);

                    if (!File.Exists(syncEpisodeJob.LocalFileName))
                    {
                        syncEpisodeJob.Message = string.Format("'{0}' is expected to exist but does not.", syncEpisodeJob.LocalFileName);
                        this.OnChanged(ManagerEventKind.SyncFileFinished, syncEpisodeJob);

                        syncJob.Message = "Episode failed to sync.";
                        this.OnChanged(ManagerEventKind.SyncProgress, syncJob);
                        continue;
                    }

                    //verify the download is complete if possible
                    //if(syncJob.Episode.FileSize != Episode.FileSizeUnknown)
                    //{
                    //    var fileInfo = new FileInfo(downloaded);
                    //    if (syncJob.Episode.FileSize != fileInfo.Length)
                    //    {
                    //        this.OnChanged(new ManagerEventArgs { Kind = ManagerEventKind.SyncFileFailure, SyncPercentComplete = CalculatePercentComplete(synced, this._syncQueue.Count), Message = string.Format("'{0}' exists but is not the correct size. File skipped.", downloaded), Episode = syncJob.Episode });
                    //        continue;
                    //    }
                    //}


                    var subDirectory = Path.GetDirectoryName(syncEpisodeJob.LocalFileName);
                    if (string.IsNullOrEmpty(subDirectory))
                    {
                        syncEpisodeJob.Message = "Could not determine source subdirectory of episode.";
                        this.OnChanged(ManagerEventKind.SyncFileFinished, syncEpisodeJob);
                        
                        this.OnChanged(ManagerEventKind.SyncProgress, syncJob);
                        continue;
                    }

                    var x = subDirectory.Remove(0, this.DataDirectory.Length);
                    var targetDirectory = Path.Combine(this.SyncDirectory, x);
                    if (!Directory.Exists(targetDirectory))
                    {
                        try
                        {
                            Directory.CreateDirectory(targetDirectory);
                        }
                        catch (IOException ex)
                        {
                            syncEpisodeJob.Error = ex;
                            syncEpisodeJob.Message = "Could not create subdirectory to sync episode to.";
                            this.OnChanged(ManagerEventKind.SyncFileFinished, syncEpisodeJob);
                            continue;
                        }
                    }

                    var destinationPathFileName = Path.Combine(targetDirectory, Path.GetFileName(syncEpisodeJob.LocalFileName));
                    try
                    {
                        if(!File.Exists(destinationPathFileName))
                        {
                            File.Copy(syncEpisodeJob.LocalFileName, destinationPathFileName);
                        }
                        lock(this._podcasts.Lock)
                        {
                            this._podcasts.AddEpisode(syncEpisodeJob.Episode);
                        }
                        stepsComplete++;
                        this.OnChanged(ManagerEventKind.SyncFileFinished, syncEpisodeJob);
                        File.Delete(syncEpisodeJob.LocalFileName);
                    }
                    catch (IOException ex)
                    {
                        syncEpisodeJob.Message = "Could not sync episode to sync target directory.";
                        syncEpisodeJob.Error = ex;
                        this.OnChanged(ManagerEventKind.SyncFileFinished, syncEpisodeJob);
                        continue;
                    }
                }
            }

            syncJob.Message = "Sync finished.";
            syncJob.StepsPercentComplete = 100;
            this.OnChanged(ManagerEventKind.SyncFinished, syncJob);       
        }

        public void Dispose()
        {
            this._episodeDownloader.CancelAsync();
            this._episodeDownloader.Dispose();

            this._feedDownloader.CancelAsync();
            this._feedDownloader.Dispose();

            this._shutdownThreads = true;

            this._downloadThread.Join();
            this._postProcessThread.Join();
        }

        void AddToFeedDownloadQueue(DownloadJob job)
        {
            lock (this._feedDownloadQueueLock)
            {
                this._feedDownloadQueue.Enqueue(job);

                this.OnChanged(new ManagerChangedEventArgs { Kind = ManagerEventKind.FileDownloadQueued, Job = job });
            }
        }

        void AddToImageDownloadQueue(ImageDownloadJob job)
        {
            lock(this._imageDownloadQueueLock)
            {
                this._imageDownloadQueue.Enqueue(job);
                this.OnChanged(new ManagerChangedEventArgs { Kind = ManagerEventKind.FileDownloadQueued, Job = job });
            }
        }

        void AddToEpisodeDownloadQueue(EpisodeDownloadJob job)
        {
            lock (this._episodeDownloadQueueLock)
            {
                if (this.AlreadyDownloaded(job))
                {
                    this.OnChanged(ManagerEventKind.SyncFileAvailable, job);
                    
                    job.ProcessingAction = this.AddToSyncQueue;
                    this.AddToProcessQueue(job);
                }
                else if (this.DoNotDownload(job))
                {
                    job.Message = "File will not be downloaded.";
                    this.OnChanged(ManagerEventKind.FileDownloadCanceled, job);
                }
                else
                {
                    this._episodeDownloadQueue.Enqueue(job);
                   if(job.DownloadUri != null)
                        this.OnChanged(ManagerEventKind.FileDownloadQueued, job);
                }
            }
        }

        QueuePriority ExtensionToPriority(string extension)
        {
            if (extension == null)
                return QueuePriority.Feed;//if we get here we are a notification placeholder, fix this
            if(".xml".Equals(extension, StringComparison.OrdinalIgnoreCase))
                return QueuePriority.Feed;
            if(".image".Equals(extension, StringComparison.OrdinalIgnoreCase))
                return QueuePriority.Image;
            return QueuePriority.Episode;
        }

        void AddToProcessQueue(Job job)
        {
            lock (this._postProcessQueueLock)
            {
                if (this._postProcessQueue != null)
                {
                    this._postProcessQueue.Enqueue(job);
                    this.OnChanged(ManagerEventKind.JobQueued, job);
                }
            }
        }

        void ConsumeDownloadQueue()
        {
            while (!this._shutdownThreads)
            {
                Thread.Sleep(this.DownloadThreadIdleMilliseconds);

                {
                    EpisodeDownloadJob episodeJob = null;

                    //consume episode queue
                    lock (this._episodeDownloadQueueLock)
                    {
                        if (this._episodeDownloadQueue.Count > 0
                            && !this._episodeDownloader.IsBusy)
                        {
                            episodeJob = this._episodeDownloadQueue.Dequeue();
                        }
                    }
                    if (episodeJob != null)
                    {
                        lock (this._podcasts.Lock)
                        {
                            this._podcasts.SetEpisodeState(episodeJob.Episode.UniqueId, EpisodeState.DownloadStarted);
                        }
                        ConsumeDownloadJob(episodeJob, this._episodeDownloader);
                    }
                }

                {
                    lock (this._imageDownloadQueueLock)
                    {
                        ImageDownloadJob imageJob = null;
                        if (this._imageDownloadQueue.Count > 0
                            && !this._imageDownloader.IsBusy)
                        {
                            imageJob = this._imageDownloadQueue.Dequeue();
                            ConsumeDownloadJob(imageJob, this._imageDownloader);
                        }
                    }
                }

                {
                    DownloadJob job = null;

                    //consume feed queue
                    lock (this._feedDownloadQueueLock)
                    {
                        if (this._feedDownloadQueue.Count > 0
                            && !this._feedDownloader.IsBusy)
                        {
                            job = this._feedDownloadQueue.Dequeue();
                        }
                    }

                    //could just be a marker
                    if (job != null
                        && job.DownloadUri != null)
                    {
                        //could be a feed or an image...
                        var feedJob = job as FeedDownloadJob;
                        if (feedJob != null)
                        {
                            lock (this._podcasts)
                            {
                                this._podcasts.SetFeedState(job.DownloadUri, FeedState.DownloadStarted);
                            }
                        }
                        ConsumeDownloadJob(feedJob, this._feedDownloader);
                    }
                }
            }
        }

        private void ConsumeDownloadJob(DownloadJob job, WebClient downloader)
        {
            try
            {
                if (job.DownloadUri == null)
                {
                    //must be a marker job that denotes the begin/end of a multijob process
                    this.AddToProcessQueue(job);
                    return;
                }
                
                if (job.DownloadingAction != null)
                    job.DownloadingAction(job);

                if (File.Exists(job.LocalFileName))
                    File.Delete(job.LocalFileName);

                this.OnChanged(ManagerEventKind.FileDownloading, job);
                downloader.DownloadFileAsync(job.DownloadUri, job.LocalFileName, job);
            }
            catch (Exception ex)
            {
                job.Error = ex;
                this.OnChanged(ManagerEventKind.Error, job);
            }
        }

        /// <summary>
        /// The time of the next automatic feed update check.
        /// </summary>
        public DateTime NextFeedRefresh
        {
            get { return this._lastFeedRefresh + TimeSpan.FromMinutes(this.FeedCheckMinutes); }
        }

        bool DoNotDownload(EpisodeDownloadJob job)
        {
            return
                  job.Episode != null
                  && (job.Episode.IsVideo && this.SkipVideoEpisodes);

        }

        bool AlreadyDownloaded(EpisodeDownloadJob episodeDownloadJob)
        {
            return
                //dont download mp3s we already have
                (episodeDownloadJob.Episode != null
                && File.Exists(episodeDownloadJob.LocalFileName))
                //&& IsCompleteDownload(job.Episode, job.LocalFileName))
                ||
                //dont download images we already have
                (File.Exists(episodeDownloadJob.LocalFileName)
                && IsImageFile(episodeDownloadJob.LocalFileName));
        }

        bool IsCompleteDownload(Episode episode, string localFileName)
        {
            var fileInfo = new FileInfo(localFileName);
            return
                episode.FileSize == Episode.FileSizeUnknown
                || episode.FileSize == fileInfo.Length;
        }

        bool IsImageFile(string localFileName)
        {
            return !string.IsNullOrEmpty(localFileName)
            && Path.GetExtension(localFileName).Equals(".image", StringComparison.OrdinalIgnoreCase);
        }

        void ConsumeProcessQueue()
        {
            while (!this._shutdownThreads)
            {
                Thread.Sleep(this.ProcessThreadIdleMilliseconds);

                Job job = null;
                lock (this._postProcessQueueLock)
                {
                    if (this._postProcessQueue != null
                        && this._postProcessQueue.Count > 0)
                    {
                        job = this._postProcessQueue.Dequeue();
                    }
                }
                if (job != null
                    && job.ProcessingAction != null)
                {
                    job.ProcessingAction(job);
                }
                else
                {
                    //TimeUntilNextFeedUpdate = this.NextFeedRefresh - DateTime.Now
                    this.OnChanged(ManagerEventKind.ManagerIdle, null);
                }

                if (DateTime.Now > this.NextFeedRefresh)
                {
                    this._lastFeedRefresh = DateTime.Now;
                    this.UpdateFeeds();
                }
            }
        }



        public void CancelCurrentDownload()
        {
            this._episodeDownloader.CancelAsync();
        }
    }

    public enum ManagerEventKind
    {
        NotSpecified,//must be first enum value
        Error,

        ManagerStarted,
        ManagerIdle,
        ManagerStopped,
        
        AddFeedStarted,
        AddFeedFinished,

        UpdateFeedStarted,
        UpdateFeedProgress,
        UpdateFeedFinished,

        UpdateFeedsStarted,
        UpdateFeedsProgress,
        UpdateFeedsFinished,

        FileDownloadQueued,
        FileDownloading,
        FileDownloadingProgress,
        FileDownloaded,

        FileDownloadCanceled,
        SyncFileAvailable,

        SyncStarted,
        SyncProgress,
        SyncFinished,

        SyncFileStarted,
        SyncFileFinished,

        JobQueued,
        FeedDataAvailable,
        EpisodeDataAvailable,

        ImportFeedsStarted,
        ImportFeedsProgress,
        ImportFeedsFinished,
        
        ExportFeedsStarted,
        ExportFeedsProgress,
        ExportFeedsFinished,
    }

    public class ManagerChangedEventArgs : EventArgs
    {
        public ManagerEventKind Kind { get; set; }
        public Job Job { get; set; }
    }

    public class FeedInfoChangedEventArgs : EventArgs
    {
        public Feed Feed { get; set; }
        public Episode Episode { get; set; }
    }

    public class Job
    {
        public Action<Job> ProcessingAction { get; set; }
        public string Message { get; set; }
        public string DownloadMessage { get; set; }
        public string ProcessMessage { get; set; }
        public Exception Error { get; set; }
    }

    public class ProgressJob : Job
    {
        public long StepsComplete { get; set; }
        public long StepsTotal { get; set; }
        public Int32 StepsPercentComplete { get; set; }
        public ManagerEventKind StepsNotifyKind { get; set; }
    }

    public class LocalFileJob : ProgressJob
    {
        public string LocalFileName { get; set; }
    }

    [DebuggerDisplay("{DownloadUri.OriginalString}")]
    public class DownloadJob : LocalFileJob
    {
        public Uri DownloadUri { get; set; }

        public Action<Job> DownloadingAction { get; set; }
        public Action<Job> DownloadedAction { get; set; }
    }

    public class ImageDownloadJob : FeedDownloadJob
    {
        
    }

    public class FeedDownloadJob : DownloadJob
    {
        public Feed Feed { get; set; }
    }

    public class EpisodeDownloadJob : DownloadJob
    {
        public Episode Episode { get; set; }
    }

    enum QueuePriority
    {
        Image,
        Feed,
        Episode
    }
}
